using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Principal;
using System.Security.Permissions;
using System.Security;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;

namespace InfiniTec.Security
{
    /// <summary>
    /// Extends the <see cref="ImpersonationScope"/> with the ability to acquire the necessary credential
    /// from via a callback function (See <see cref="AcquireCredentialEventArgs"/>). Additionally, this
    /// class integrates the <see cref="CredentialManager"/> to temporarily save credentials.
    /// </summary>
    public class CallbackImpersonationScope: ImpersonationScope
    {
        private string _TargetName;
        private EventHandler<AcquireCredentialEventArgs> _CredentialCallback;

        private void EnsureValidCrendentialEntry()
        {
            EnsureValidCrendentialEntry(0);
        }

        private void EnsureValidCrendentialEntry(int attemptCount)
        {
            if (CredentialEntry == null && !AcquireCredential(attemptCount)) HandleInvalidCredential(null);
        }

        /// <summary>
        /// Gets or sets the <see cref="AcquireCredentialEventArgs"/> method that is called whenever
        /// a valid username/password combination cannot be found in the <see cref="CredentialManager"/>.
        /// </summary>
        protected EventHandler<AcquireCredentialEventArgs> CredentialCallback
        {
            get { return _CredentialCallback; }
            set { _CredentialCallback = value; }
        }

        /// <summary>
        /// Impersonates as another user. Calls the specified <see cref="AcquireCredentialEventArgs"/>
        /// method to get a valid username/password combination, if no credential set can be found
        /// in the <see cref="CredentialManager"/> for the given target name.
        /// </summary>
        public override sealed void Impersonate()
        {
            int attemptCount = 1;

            EnsureValidCrendentialEntry();

            do
            {
                try
                {
                    base.Impersonate();
                    
                    CacheCredential();
                    break;
                }
                catch (InvalidCredentialException)
                {
                    Invalidate();
                    EnsureValidCrendentialEntry(attemptCount);
                }
                attemptCount++;
            } while (true);
        }

        /// <summary>
        /// Creates a new instance of this class and impersonates as the user specified by targetName.
        /// </summary>
        /// <param name="targetName">The targetName of the identity, as stored in the <see cref="CredentialManager"/>,</param>
        /// <param name="logonType">The <see cref="LogonType"/> to use for impersonation.</param>
        /// <param name="callback">This callback is invoked whenever the username and password are no longer available.</param>
        public CallbackImpersonationScope(string targetName, EventHandler<AcquireCredentialEventArgs> callback, LogonType logonType) : this(targetName, callback, logonType, true) { }

        /// <summary>
        /// Creates a new instance of this class and impersonates as the user specified by targetName.
        /// </summary>
        /// <param name="targetName">The targetName of the identity, as stored in the <see cref="CredentialManager"/>,</param>
        /// <param name="callback">This callback is invoked whenever the username and password are no longer available.</param>
        public CallbackImpersonationScope(string targetName, EventHandler<AcquireCredentialEventArgs> callback) : this(targetName, callback, LogonType.Interactive) { }

        /// <summary>
        /// Creates a new instance of this class and impersonates as the user specified by targetName.
        /// </summary>
        /// <param name="targetName">The targetName of the identity, as stored in the <see cref="CredentialManager"/>,</param>
        /// <param name="callback">This callback is invoked whenever the username and password are no longer available.</param>
        /// <param name="logonType">The <see cref="LogonType"/> to use for impersonation.</param>
        /// <param name="impersonateImmediately">If true, the impersonation in executed immediately.</param>
        public CallbackImpersonationScope(string targetName, EventHandler<AcquireCredentialEventArgs> callback, LogonType logonType, bool impersonateImmediately)
        {
            if (string.IsNullOrEmpty(targetName)) throw new ArgumentException("targetName must not be empty");

            LogonType = logonType;
            _CredentialCallback = callback;
            _TargetName = targetName;

            if (impersonateImmediately) Impersonate();
        }

        /// <summary>
        /// Starts the given process under the current impersonation Scope.
        /// </summary>
        /// <param name="info">The process info to use</param>
        public override Process StartProcess(ProcessStartInfo info)
        {
            int attemptCount = 1;
            Process result;

            EnsureValidCrendentialEntry();

            result = null;

            do
            {
                try
                {
                    base.StartProcess(info);

                    CacheCredential();
                    break;
                }
                catch (InvalidCredentialException)
                {
                   Invalidate();
                   EnsureValidCrendentialEntry(attemptCount);
                }
                attemptCount++;
            } while (true);

            return result;
        }

        private void HandleInvalidCredential(Exception ex)
        {
            throw new CredentialInputCancelledException(_TargetName, "No valid credential was provided.", ex);
        }

        /// <summary>
        /// Invalidates the current credential in use. The next time a credential is
        /// needed, it is requeried.
        /// </summary>
        public virtual void Invalidate()
        {
            CredentialManager.Remove(_TargetName);
            CredentialEntry = null;
        }

        /// <summary>
        /// Caches the credential in the credential cache for future use.
        /// </summary>
        public virtual void Confirm()
        {
            CacheCredential();
        }

        private void CacheCredential()
        {
            if (!CredentialManager.Contains(_TargetName)) CredentialManager.Add(_TargetName, CredentialEntry);
        }

        private bool AcquireCredential(int attemptCount)
        {
            AcquireCredentialEventArgs e;

            CredentialEntry = CredentialManager.GetIdentity(_TargetName);

            if (CredentialEntry != null) return true;

            if (_CredentialCallback == null) throw new ArgumentException("No callback specified!", "callback");

            e = new AcquireCredentialEventArgs(_TargetName, attemptCount);

            _CredentialCallback(this, e);

            if (e.Cancel) return false;

            CredentialEntry = new CredentialEntry(e.Username, e.Password, LogonType);

            return true;
        }
    }
}
